/*******************************************************************************
 * Copyright (c) 2011 TXT e-solutions SpA
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * This work was performed within the IoT_at_Work Project
 * and partially funded by the European Commission's
 * 7th Framework Programme under the research area ICT-2009.1.3
 * Internet of Things and enterprise environments.
 *
 *
 * Authors:
 *     Cristoforo Seccia (TXT e-solutions SpA)
 *
 * Contributors:
 *      Domenico Rotondi (TXT e-solutions SpA)
 *******************************************************************************/
package it.txt.access.capability.demo.server.tcp;

import it.txt.access.capability.commons.schema.CapabilityNamespacesMapper;
import it.txt.access.capability.commons.signer.X509DocumentSigner;
import it.txt.access.capability.commons.utils.ClientServerUtils;
import it.txt.access.capability.demo.schema.CapabilityResponseType;
import it.txt.access.capability.demo.schema.factory.CapabilityDemoFactoryException;
import it.txt.access.capability.demo.schema.response.factory.CapabilityDemoResponseFactory;
import it.txt.access.capability.demo.server.view.model.ServerResponseInfoModel;
import it.txt.access.capability.demo.server.view.model.ServiceRequestInfoModel;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 *
 * @author Cristoforo Seccia (TXT e-solutions SpA)
 */
public class ResponseClient implements Runnable {

    private static final Logger LOGGER = Logger.getLogger(ResponseClient.class.getName());
    private ResponseClientSupport progressSupport;
    private DataOutputStream outChannel;
    private DataInputStream inChannel;
    private Socket clientSocket;

    public ResponseClient(Socket socket, ResponseClientSupport support) {
        this.clientSocket = socket;
        this.progressSupport = support;
    }

    public void run() {
        //send the connection response.
        if (!sendConnectionResponse()) {
            //Fire new event connection enstablished error.
            fireConnectionWithClientErrorEvent();
            //Close the client socket.
            closeClientSocket();
            return;
        }
        //Fire new event connection enstablished success.
        fireConnectionWithClientSuccessEvent();
        //Receive the client request.
        XMLInputStream xmlis = receiveClientRequest();
        if (xmlis.getDataReceivedLenght() == XMLInputStream.INVALID_LENGHT) {
            //Fire new event service request error.
            fireReceivedRequestErrorEvent();
            //closing the client socket.
            closeClientSocket();
            return;
        }
        //init a new server response info model
        ServerResponseInfoModel responseInfoModel = new ServerResponseInfoModel();
        //Recover the received data
        final byte[] receivedData = xmlis.getReceivedData();
        //the response message sent to the client
        String responseMessage = "";
        try {
            //check the received client request.
            ServiceRequestInfoModel requestInfoModel = checkRequestCapability(receivedData);
            //Strore the request ID into the response info model.
            responseInfoModel.setRequestID(requestInfoModel.getRequestID());
            //Store the vslid request status into the response info model.
            responseMessage += ClientServerUtils.RESPONSE_CAPABILITY_VALID;
            //Fire new event service request Info.
            fireReceivedRequestInfo(requestInfoModel);
            try {
                //Now try to perform the requested operation
                ResponseClientHelper.performRequestOperation(requestInfoModel);
                //The operation was performed. Append to the status response
                //the operation performed message.
                responseMessage += " " + ClientServerUtils.RESPONSE_REQUEST_OPERATION_PERFORMED;
                //Fire new event service request received ok.
                fireReceivedRequestSuccessEvent();
            } catch (RequestOperationClientException ex) {
                //The request contains an invalid operation.
                LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
                //Fire new event service request error.
                fireReceivedRequestErrorEvent();
                //Store the error response status into the response info model.
                responseMessage += " " + ex.getMessage();
            }
        }
        catch (RequestClientException ex) {
            //The request contains an invalid capability.
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
            //Fire new event service request error.
            fireReceivedRequestErrorEvent();
            //Store invalid request ID into the response info model.
            responseInfoModel.setRequestID(ClientServerUtils.INVALID_REQUEST_ID);
            //Store the error response status into the response info model.
            responseMessage = ClientServerUtils.RESPONSE_CAPABILITY_INVALID;
            responseMessage += " " + ex.getMessage();
        }
        //Store the final request status into the response info model.
        responseInfoModel.setResponseStatus(responseMessage);
        //Prepare the signed response to the client and recover the response ID.
        XMLOutputStream xmlos = prepareXMLResponse(responseInfoModel);
        //check if the xml data has been writted to the sokect
        if (xmlos.getDataToSendLenght() == XMLOutputStream.EMPTY_LENGHT) {
            //Store the internal error status into the response info model.
            String errorMessage = responseMessage;
            errorMessage += " " + ClientServerUtils.RESPONSE_INTERNAL_ERROR;
            //Respond with a plain error message.
            if (sendPlainResponse(errorMessage)) {
                //Fire new event service response success.
                fireSendingResponseSuccessEvent();
            }
            else {
                //Fire new event service response success.
                fireSendingResponseErrorEvent();
            }
        }
        else {
            if (sendXMLResponse(xmlos)) {
                //Fire new event service response success.
                fireSendingResponseSuccessEvent();
            }
            else {
                //Fire new event service response success.
                fireSendingResponseErrorEvent();
            }
        }
        //Fire a new service response info event.
        fireSendingResponseInfo(responseInfoModel);
        //Close in/out streams and client socket
        closeClientSocket();
    }

    private boolean sendConnectionResponse() {
        boolean result = false;
        try {
            LOGGER.log(Level.INFO, "Sending connection response...");
            inChannel = new DataInputStream(clientSocket.getInputStream());
            outChannel = new DataOutputStream(clientSocket.getOutputStream());
            byte[] data = ClientServerUtils.RESPONSE_CONNECTION_SUCCESS.getBytes();
            outChannel.writeInt(data.length);
            outChannel.write(data);
            outChannel.flush();
            LOGGER.log(Level.INFO, "Connection response sent.");
            result = true;
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        return result;
    }

    private XMLInputStream receiveClientRequest() {
        XMLInputStream xmlis = new XMLInputStream(inChannel);
        try {
            LOGGER.log(Level.INFO, "Waiting fot the client request...");
            xmlis.recive();
            LOGGER.log(Level.INFO, "Client request received.");
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        return xmlis;
    }

    private ServiceRequestInfoModel checkRequestCapability(final byte[] receivedData) throws RequestClientException {
        //Request received now get the cabaility request and make controls.
        ServiceRequestInfoModel requestInfoModel = null;
        LOGGER.log(Level.INFO, "Verifying request capability...");
        ByteArrayInputStream inputStream = new ByteArrayInputStream(receivedData);
        requestInfoModel = ResponseClientHelper.checkRequestCapability(inputStream);
        //Recover the request ID.
        String requestID = requestInfoModel.getRequestID();
        LOGGER.log(Level.INFO, "Capability Verify OK. - Request ID: {0}", requestID);
        return requestInfoModel;
    }

    /**
     * @param responseInfoModel The response model info which will be modified by
     * adding the genersted response ID value.
     */
    private XMLOutputStream prepareXMLResponse(ServerResponseInfoModel infoModel) {

        XMLOutputStream xmlos = new XMLOutputStream();

        String respID = ClientServerUtils.INVALID_RESPONSE_ID;

        LOGGER.log(Level.INFO, "Creating Signed XML response...");

        try {

            //Creating the signed xml response
            CapabilityResponseType responseType =
                    CapabilityDemoResponseFactory.getInstance().createResponse(
                    infoModel.getResponseStatus(), infoModel.getRequestID());

            Document signedResponseDoc =
                    CapabilityDemoResponseFactory.getInstance().createResponseDocument(
                    responseType);

            X509DocumentSigner.signXMLElement(signedResponseDoc.getDocumentElement(),
                    this.progressSupport.getCertificateKeyValues(),
                    CapabilityDemoResponseFactory.SIGN_DOCUMENT_AFTER_NODE);

            //recover the generated responseID from the xml
            respID = recoverResponseID(signedResponseDoc.getDocumentElement());
            //Writing XML response into the socket
            LOGGER.log(Level.INFO, "Writing Signed XML response to the client socket...");
            DOMSource source = new DOMSource(signedResponseDoc.getDocumentElement());
            StreamResult result = new StreamResult(xmlos);
            TransformerFactory transFactory = TransformerFactory.newInstance();
            Transformer transformer = transFactory.newTransformer();
            transformer.transform(source, result);            
        }
        catch(CapabilityDemoFactoryException ex){
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        catch (TransformerException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        catch (GeneralSecurityException ex){
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        //Store the response ID
        infoModel.setResponseID(respID);
        return xmlos;
    }

    private boolean sendXMLResponse(XMLOutputStream xmlos) {
        LOGGER.log(Level.INFO, "Sending signed XML response...");
        try {
            //The data hab been writted successfully
            int dataToSent = xmlos.getDataToSendLenght();
            LOGGER.log(Level.INFO, "Writted Data lenght: {0}", dataToSent);
            //Send the xml response
            xmlos.send(outChannel);
            LOGGER.log(Level.INFO, "Data sent lenght: {0}", xmlos.getDataSentLenght());
            //Check that the xml response has been correctly sent.
            if (xmlos.getDataSentLenght() == dataToSent) {
                LOGGER.log(Level.INFO, "Signed XML response correctly sent.");
                return true;
            } else {
                LOGGER.log(Level.INFO, "Error while sending XML response.");
                return false;
            }
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
            return false;
        }
    }

    private boolean sendPlainResponse(String plainMassage) {
        LOGGER.log(Level.INFO, "Sending Plain Response: {0}", plainMassage);
        //send the plain response
        try {
            byte[] data = plainMassage.getBytes();
            outChannel.writeInt(data.length);
            outChannel.write(data);
            outChannel.flush();
            LOGGER.log(Level.INFO, "Plain Response sent");
            return true;
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
            return false;
        }
    }

    private void fireConnectionWithClientSuccessEvent() {
        //Fire new event connection enstablished ok.
        progressSupport.fireCapabilityServerEvent(
                ResponseClientSupport.EVENT_NAME_TCP_CONNECTION,
                ResponseClientSupport.STATE_SETTINGS,
                ResponseClientSupport.STATE_OK);
    }

    private void fireConnectionWithClientErrorEvent() {
        //Fire new event connection enstablished error.
        progressSupport.fireCapabilityServerEvent(
                ResponseClientSupport.EVENT_NAME_TCP_CONNECTION,
                ResponseClientSupport.STATE_SETTINGS,
                ResponseClientSupport.STATE_ERROR);
    }

    private void fireReceivedRequestSuccessEvent() {
        //Fire new event service request received ok.
        progressSupport.fireCapabilityServerEvent(
                ResponseClientSupport.EVENT_NAME_SERVICE_REQUEST_RECEIVED,
                ResponseClientSupport.STATE_SETTINGS,
                ResponseClientSupport.STATE_OK);
    }

    private void fireReceivedRequestErrorEvent() {
        //Fire new event service request received error.
        progressSupport.fireCapabilityServerEvent(
                ResponseClientSupport.EVENT_NAME_SERVICE_REQUEST_RECEIVED,
                ResponseClientSupport.STATE_SETTINGS,
                ResponseClientSupport.STATE_ERROR);
    }

    private void fireReceivedRequestInfo(ServiceRequestInfoModel infoModel) {
        //Fire new event service request received info.
        progressSupport.fireCapabilityRequestInfo(infoModel);
    }

    private void fireSendingResponseSuccessEvent() {
        //Fire new event service response sent ok.
        progressSupport.fireCapabilityServerEvent(
                ResponseClientSupport.EVENT_NAME_SERVICE_RESPONSE_SENT,
                ResponseClientSupport.STATE_SETTINGS,
                ResponseClientSupport.STATE_OK);
    }

    private void fireSendingResponseErrorEvent() {
        //Fire new event service response sent error.
        progressSupport.fireCapabilityServerEvent(
                ResponseClientSupport.EVENT_NAME_SERVICE_RESPONSE_SENT,
                ResponseClientSupport.STATE_SETTINGS,
                ResponseClientSupport.STATE_ERROR);
    }

    private void fireSendingResponseInfo(ServerResponseInfoModel infoModel) {
        //Fire new event service request received info.
        progressSupport.fireCapabilityResponseInfo(infoModel);
    }

    private String recoverResponseID(Element signedResponse) {
        String response = ClientServerUtils.INVALID_RESPONSE_ID;
        LOGGER.log(Level.INFO, "Getting the server response ID from the XML respose...");
        NodeList nl = signedResponse.getElementsByTagNameNS(
                CapabilityNamespacesMapper.CAPABILITY_DEMO_RESPONSE_QNAME.getNamespaceURI(),
                "CapabilityResponseID");
        LOGGER.log(Level.INFO, "Server response ID elememts found: {0}", nl.getLength());
        if (nl.getLength() != 0) {
            response = nl.item(0).getTextContent();
        }
        return response;
    }

    private void closeClientSocket() {
        try {
            //Close the client socket.
            if (clientSocket != null) {
                LOGGER.log(Level.INFO, "Closing client socket...");
                inChannel.close();
                outChannel.close();
                clientSocket.close();
            }
            LOGGER.log(Level.INFO, "Client socket closed.");
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
    }

    public class XMLInputStream extends ByteArrayInputStream {

        private byte[] receivedData;
        private DataInputStream inChannel;
        public static final int INVALID_LENGHT = -1;
        private int receivedDataLenght = INVALID_LENGHT;

        public XMLInputStream(DataInputStream inChannel) {
            super(new byte[0]);
            this.inChannel = new DataInputStream(inChannel);
        }

        public int getDataReceivedLenght() {
            return receivedDataLenght;
        }

        public byte[] getReceivedData() {
            return receivedData;
        }

        public void recive() throws IOException {
            this.receivedDataLenght = INVALID_LENGHT;
            this.receivedDataLenght = this.inChannel.readInt();
            LOGGER.log(Level.INFO, "Received {0} of data from the client socket.", receivedDataLenght);
            this.receivedData = new byte[receivedDataLenght];
            this.inChannel.read(receivedData, 0, receivedDataLenght);
            this.buf = receivedData;
            this.count = receivedDataLenght;
            this.mark = 0;
            this.pos = 0;
        }
    }

    public class XMLOutputStream extends ByteArrayOutputStream {

        public static final int EMPTY_LENGHT = 0;
        public static final int INVALID_LENGHT = -1;
        private int dataSentLenght = INVALID_LENGHT;

        public int getDataToSendLenght() {
            return this.count;
        }

        public int getDataSentLenght() {
            return dataSentLenght;
        }

        public void send(DataOutputStream outChannel) throws IOException {
            dataSentLenght = INVALID_LENGHT;
            byte[] dataSent = toByteArray();
            LOGGER.log(Level.INFO, "Sending {0} of data to the socket.", dataSent.length);
            outChannel.writeInt(dataSent.length);
            outChannel.write(dataSent);
            outChannel.flush();
            dataSentLenght = dataSent.length;
            reset();
        }
    }
}
